package com.gusto.engine.semsim.measures.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.log4j.Logger;

import com.gusto.engine.beans.exceptions.PropertyAccessException;
import com.gusto.engine.beans.model.Res;
import com.gusto.engine.semsim.exceptions.SimilarityException;
import com.gusto.engine.semsim.measures.ResourceSimilarity;
import com.opensymphony.oscache.base.Cache;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;

/**
 * <p>Wu &amp; Palmer formula <br/>
 * {@code similarity = 2 * depthCommonParent / (2 * depthCommonParent + depth1 + depth2)}.<br/>
 * <ul>
 * 	<li>depth1 the depth between element1 and commonParent
 *  <li>depth2 the depth between element2 and commonParent
 * </ul></p>
 * @author amokrane.belloui@gmail.com
 */
public class EdgeCountingSimilarity implements ResourceSimilarity {
	
	private Logger log = Logger.getLogger(EdgeCountingSimilarity.class);
	
	private Long depth = 1L;
	public Long getDepth() {
		return depth;
	}
	public void setDepth(Long depth) {
		this.depth = depth;
	}
	
	private String stereotype;
	public String getStereotype() {
		return stereotype;
	}
	public void setStereotype(String stereotype) {
		this.stereotype = stereotype;
		
		if (stereotype.equals("SEMANTIC")) {
			parentProperties.add("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
			parentProperties.add("http://www.w3.org/2000/01/rdf-schema#subClassOf");
			stopResources.add("http://www.w3.org/2000/01/rdf-schema#Resource");
			stopResources.add("http://www.w3.org/2002/07/owl#Class");
			stopResources.add("http://www.w3.org/2000/01/rdf-schema#Class");
		}
	}
	
	private List<String> parentProperties = new ArrayList<String>();
	private List<String> stopResources = new ArrayList<String>();
	
	private Cache cache;
	
	public EdgeCountingSimilarity() {
		super();
		Properties props = new Properties();
		props.put("cache.memory", true);
		props.put("cache.capacity", 10000000);
		props.put("cache.algorithm", "com.opensymphony.oscache.base.algorithm.LRUCache");
		
		GeneralCacheAdministrator ca = new GeneralCacheAdministrator(props);
		this.cache = ca.getCache();
	}
	
	public Double getSimilarity(Res value1, Res value2) throws SimilarityException, PropertyAccessException {
		String key = "sim." + value1 + "." + value2;
		Double sim;
		try {
			sim = (Double) cache.getFromCache(key);
			log.debug("Load Edge Counting from Cache (" + key + ")");
		} 
		catch (com.opensymphony.oscache.base.NeedsRefreshException nre) {
		    try {
				sim = getSimilarityImpl(value1, value2);
				cache.putInCache(key, sim);
				log.debug("Calculate Edge Counting with no cache (" + key + ")");
		    } 
		    catch (Exception ex) {
		    	sim = (Double) nre.getCacheContent();
		        cache.cancelUpdate(key);
		        log.debug("Load Edge Counting from Cache after Exception in Calculating Similarity (" + key + ")", ex);
		    }
		}
	    return sim;
	}
	
	private Double getSimilarityImpl(Res value1, Res value2) throws SimilarityException, PropertyAccessException {
		if (value1.equals(value2)) {
			return 1.0;
		}
		else {
			return getSimilarityImpl2(value1, value2);
		}
	}
	
	private Double getSimilarityImpl2(Res value1, Res value2) throws SimilarityException, PropertyAccessException {
		Res save1 = value1;
		Res save2 = value2;
		
		Path path1 = new Path();
		Path path2 = new Path();
		boolean found = false;
		
		boolean parentsAvailable = true;
		
		Double depth1 = 0D;
		Double depth2 = 0D;
		Res commonParent = null;
		Double depthR = 0D;
		Double sim = 0D;
		
		while (!found && parentsAvailable) {
			boolean parentsAvailable1 = true;
			boolean parentsAvailable2 = true;
			
			// Parent 1
			Res parent1 = getParent(value1);
			// Parent 1 Push
			if (parent1 != null) {
				path1.pushElementToPath(parent1.toString());
				log.debug("Process Parent : " + value1 + " -> " + parent1);
				value1 = parent1;
			}
			else {
				parentsAvailable1 = false;
			}
			// Check if exists in 2
			if (path2.containsElement(value1.toString())) {
				found = true;
				
				depth1 = path2.elementDepth(value1.toString()) * 1.0;
				depth2 = path1.elementDepth(value1.toString()) * 1.0;
				depthR = getDepth(value1) * 1.0;
				commonParent = value1;
			}
			else {
				// Parent 2
				Res parent2 = getParent(value2);
				// Parent 2 Push
				if (parent2 != null) {
					path2.pushElementToPath(parent2.toString());
					log.debug("Process Parent : " + value2 + " -> " + parent2);
					value2 = parent2;
				}
				else {
					parentsAvailable2 = false;
				}
				// check if exists in 1
				if (path1.containsElement(value2.toString())) {
					found = true;
					
					depth1 = path1.elementDepth(value2.toString()) * 1.0;
					depth2 = path2.elementDepth(value2.toString()) * 1.0;
					depthR = getDepth(value2) * 1.0;
					commonParent = value2;
				}
			}
			if ((!parentsAvailable1) && (!parentsAvailable2)) {
				parentsAvailable = false;
			}
		}
		
		// Calculating Similarity
		if (commonParent != null) {
			sim = 2*depthR / (2*depthR + depth1 + depth2);
		}
		
		log.debug("Path 1 Length : " + depth1);
		log.debug("Path 2 Length : " + depth2);
		log.debug("Common Parent : " + ( commonParent != null ? commonParent.toString() : "none"));
		log.debug("Common Parent Depth : " + depthR);
		
		log.info("Edge Counting : " + save1 + " " + save2 + " (" + sim + ")");
		return sim;
	}
	
	protected Integer getDepth(Res resource) throws PropertyAccessException {
		Integer depth = 0;
		while (resource != null) {
			resource = getParent(resource);
			log.debug("Depth Calculation parent : " + resource);
			depth ++;
		}
		return depth;
	}
	
	protected Res getParent(Res value1) throws PropertyAccessException {
		boolean foundParent = false;
		Iterator<String> iter = parentProperties.iterator();
		while (!foundParent && iter.hasNext()) {
			String prop = iter.next();
			Res node = value1.getResourceProperty(prop);
			if (node != null) {
				if (!stopResources.contains(node.toString())) {
					foundParent = true;
					return node;
				}
			}
		}
		return null;
	}
	
	/*
	protected boolean isCategoryZZZ(Res value1) {
		try {
			Res par = value1.getResourceProperty(this.PARENT_PROPERTY);
			
			if (par != null) {
				return true;
			}
		}
		catch (Exception ex) {
			return false;
		}
		return false;
	}
	
	protected boolean isLeafZZZ(Res value1) {
		try {
			return !isCategoryZZZ(value1);
		}
		catch (Exception ex) {
			ex.printStackTrace();
			return false;
		}
	}
	*/
	
	private class Path {
		private long actualLength = 0;
		private Map<String, Long> pathMap = new HashMap<String, Long>();
		
		public Path() {
		}
		
		public void pushElementToPath(String element) {
			actualLength ++;
			pathMap.put(element, actualLength);
		}
		
		public boolean containsElement(String element) {
			return pathMap.containsKey(element);
		}
		
		public Long elementDepth(String element) {
			return pathMap.get(element);
		}
		
	}
	
	public String getId() {
		String id = "[" + getClass().getName() + "|" + depth + "]";
		return id;
	}
	
	public List<String> getParentProperties() {
		return parentProperties;
	}
	
	public void setParentProperties(List<String> parentProperties) {
		this.parentProperties = parentProperties;
	}
	
	public List<String> getStopResources() {
		return stopResources;
	}
	
	public void setStopResources(List<String> stopResources) {
		this.stopResources = stopResources;
	}
	
}
